const path = require('path');
const compressing = require('compressing');
const { VARIABLE_KIND, VARIABLE_SCOPE_KIND } = require('smart-rocket-unit');
// eslint-disable-next-line import/no-extraneous-dependencies
const puppeteer = require('puppeteer');
const probeImage = require('probe-image-size');
const randomString = require('randomstring');
const fs = require('fs-extra');
const ProgressIndicator = require('../../../progress-indicator');
const { versionType } = require('../../../project-versions');
const BaseReport = require('../base-report');
const classCreator = require('../base-report-test-case-transformer');
const { coverageKind, resultKind } = require('../../../tests');
const testModels = require('../../../tests');
const functionModels = require('../../../functions');
const utility = require('../../../utility');
const logger = require('../../../logger');
const Token = require('../../../mongo/token');

class Report601 extends BaseReport {
  constructor(versionId, locale, version = null, project = null, creator = null, outputDir = null, format = null, complete = false) {
    super(versionId, locale, version, project, creator, outputDir, null, format);
    this.baseDir = __dirname;
    this.complete = complete;
    this.ReportTestCaseTransformer = classCreator('unitTest');

    this.fileMap = new Map();
    this.testRecords = {};
    this.coverageKind = [];

    this.accessToken = null;

    this.imagesDic = [];
  }

  /**
   * 报告生成
   */
  async generate(progressIndicator = new ProgressIndicator()) {
    const [progress1, progress2] = progressIndicator.split([0.7, 0.3]);
    const result = {
      cachePath: this.outputDir,
    };
    // 只展示这3种
    const nameMap = {
      statement: '语句',
      branch: '分支',
      mcdc: 'MC/DC',
    };
    Object.keys(nameMap).forEach((key) => {
      if (this.version.coverages & coverageKind[key]) {
        this.coverageKind.push({
          key,
          name: nameMap[key],
        });
      }
    });
    const token = await Token.findOne({ userId: this.creator._id });
    if (!token) {
      throw new Error('token not found');
    }
    this.accessToken = token.accessToken;
    this.overridingGetAreas();
    await this.spliteByFile(null, progress1);
    await this.generateSummaryReport();
    progress2.advance(0.5);
    const fileName = `${this.project.projectName}-${this.version.versionName}.${this.getReportName()}.${this.format}.zip`;
    result.reportPath = path.join(this.outputDir, fileName);
    const reportsDir = path.join(this.outputDir, this.project.projectName);
    await compressing.zip.compressDir(reportsDir, result.reportPath);
    progress2.finish();
    return result;
  }

  async generateSummaryReport() {
    const template = 'summary';
    const output = path.join(
      this.outputDir,
      this.project.projectName,
      this.version.versionName,
      `${this.project.projectName + (this.isIntegrationTest() ? '集成' : '单元')}测试报告`
    );

    const functionList = [];
    let globalIndex = 0;
    let functionIndex = 0;
    this.testRecords.files.forEach((file) => {
      const { functions } = file;
      functions.forEach((func) => {
        const { functionName, tests } = func;
        tests.forEach((t) => {
          globalIndex += 1;
          t.globalIndex = globalIndex;
          t.tableTitle = `表${globalIndex} ${functionName} 测试用例${t.testIndex}`;
          t.testName = `${functionName} 测试用例${t.testIndex}`;
          t.identifier = `XXX-HS-${functionName}-UTC-${t.testIndex}`;

          const ioArray = [];
          t.ios = ioArray;
          for (let i = 0; i < t.input.variables.length || i < t.output.length; i += 1) {
            const input = t.input.variables[i];
            const output = t.output[i];
            const item = {
              inputVariableName: '',
              outputVariableName: '',
              inputValue: '',
              outputValue: '',
              result: '',
            };
            if (input) {
              item.inputVariableName = input.variableName;
              item.inputValue = input.value;
            }
            if (output) {
              item.outputVariableName = output.variableName;
              item.outputValue = output.value;
              item.result = output.result;
            }
            ioArray.push(item);
          }
        });
        functionIndex += 1;
        func.functionIndex = functionIndex;
        functionList.push(func);
      });
    });

    return this.generateReport(template, {
      functionList,
    }, output, { imagesDic: this.imagesDic });
  }

  filterTestCasesByResultKind() {
    return false;
  }

  async handleFile(file, versionDir) {
    this.fileMap.set(file._id.toString(), file.fileName);
    const result = await super.handleFile(file, versionDir);
    await this.collectTestRecords(result);
  }

  static versionTypeSupport(vType) {
    return vType === versionType.unitTest || vType === versionType.integrationTest;
  }

  async collectTestRecords(fileTestData) {
    const { file, functions, typeSystem } = fileTestData;
    if (!this.testRecords.files) {
      this.testRecords.files = [];
    }
    const collectFile = {
      fileName: file.fileName,
      functions: [],
    };
    if (!functions || functions.length === 0) {
      return;
    }
    const newFuncs = functions.sort((a, b) => (a.index > b.index ? 1 : -1));
    const collectFuncs = await utility.arrayChunkOperation(newFuncs, 20, async (func) => {
      const collectFunc = {
        index: func.index,
        functionName: func.functionName,
        tests: [],
      };
      if (!func.tests || func.tests.length === 0) {
        return;
      }
      const coverages = [];
      this.coverageKind.forEach(({ key, name }) => {
        const covered = func[`${key}Covered`] || 0;
        const total = func[`${key}Total`] || 0;
        const value = total ? Math.round((covered / total) * 100, 10) : '-';
        coverages.push({
          name,
          value,
          valueStr: value === '-' ? value : `${value}%`,
        });
      });
      collectFunc.coverages = coverages;
      collectFunc.coveragesStr = coverages.map(({ name, valueStr }) => `${name}覆盖率 ${valueStr}`).join('、');
      const { branches, decisions, nodes } = await functionModels.getFunctionCFG(func._id);
      const functionVariables = functionModels.getFunctionVariableJson(func.variables);
      const { output: cfg, size } = await this.generateCfg(func._id);
      const cfgImageKey = 'cfgImage';
      collectFunc[cfgImageKey] = cfg;
      this.imagesDic.push({
        target: collectFunc,
        key: cfgImageKey,
        value: cfg,
        size,
      });
      let testIndex = 1;
      func.tests.forEach((test) => {
        // 这几个不统计
        if (test.resultKind === resultKind.runtimeError
          || test.resultKind === resultKind.timeout
          || test.resultKind === resultKind.systemError
          || test.resultKind === resultKind.instrumentError
          || test.resultKind === resultKind.compileError) {
          return;
        }
        const newTestcase = testModels.getJson(test, this.i18n);
        const transformer = new this.ReportTestCaseTransformer(
          newTestcase,
          functionVariables,
          typeSystem,
          this.i18n
        );
        try {
          transformer.transform(this.complete);
        } catch (error) {
          logger.error(`testCase transform error: fileName: ${this.fileMap.get(func.fileId.toString())} function name: ${func.functionName} test case number: ${test.number}:`, error);
          return;
        }
        const testcaseId = test._id.toString();
        const branchExprs = [];
        if (branches
            && Object.keys(branches).length > 0
            && nodes
            && Object.keys(nodes).length > 0
            && decisions
            && Object.keys(decisions).length > 0) {
          Object.keys(branches).forEach((branchKey) => {
            const branch = branches[branchKey];
            if (branch.decisionId === undefined || !decisions[branch.decisionId]) {
              return;
            }
            const { expr } = decisions[branch.decisionId];
            Object.keys(branch.branches).forEach((nodeId) => {
              const branchNode = branch.branches[nodeId];
              if (branchNode
                  && branchNode.kind === 'BOOLEAN'
                  && branchNode.testIds
                  && branchNode.testIds[testcaseId]
                  && branchNode.testIds[testcaseId] > 0
                  && nodes[nodeId]
                  && nodes[nodeId].begin) {
                branchExprs.push({
                  expr,
                  value: branchNode.value === '1',
                });
              }
            });
          });
        }
        const newInputDetails = {
          variables: [],
          stubs: [],
        };
          // output顺序 函数返回值，全局变量，局部静态变量，指针目标
        const newOutputDetails = [];
        // 处理桩函数打桩情况
        transformer.input.forEach((input) => {
          // 处理异常格式的字符，防止导入报告失败
          input.variable = input.variable.replace(this.unusualCharReg, '');
          // 处理变量值显示格式
          if (input.value) {
            input.value = ` = ${input.value}`;
          }
          if (input.category && input.variable) {
            newInputDetails.stubs.push({
              category: `${input.category}:`,
              value: '',
              variable: '',
            });
            newInputDetails.stubs.push({
              category: '',
              value: input.value,
              variable: input.variable,
              values: input.values,
            });
          } else {
            newInputDetails.stubs.push(input);
          }
        });
        // 处理输入
        [
          ...transformer.paramVariablesData,
          ...transformer.globalVariablesData,
          ...transformer.staticsData,
        ].forEach((variable) => {
          // 局部静态变量需要特殊处理
          if (variable.kind === VARIABLE_KIND.statics) {
            variable.baseVariableName = variable.name;
          }
          this.variableRecursion(variable, newInputDetails.variables, VARIABLE_SCOPE_KIND.input);
        });
        // 处理输出
        [
          ...transformer.returnValueData,
          ...transformer.globalOutputData,
          ...transformer.staticsOutputData,
          ...transformer.mallocOutputData].forEach((variable) => {
          // 局部静态变量需要特殊处理
          if (variable.kind === VARIABLE_KIND.statics) {
            variable.baseVariableName = variable.name;
          }
          this.variableRecursion(variable, newOutputDetails, VARIABLE_SCOPE_KIND.output);
        });
        const excuted = test.resultKind !== resultKind.unknown;
        const passed = [
          testModels.resultKind.succeed,
          testModels.resultKind.noResult,
          testModels.resultKind.nonStrictSucceed,
        ].includes(test.resultKind);
        const failed = excuted && !passed;
        // 打桩模块
        const stubModules = [];
        if (newTestcase.data.stubs) {
          Object.keys(newTestcase.data.stubs).forEach((stubName) => {
            // 只要设置了值或者打桩代码的
            if (newTestcase.data.stubs[stubName]['@value'] || newTestcase.data.stubs[stubName].body) {
              stubModules.push(stubName);
            }
          });
        }

        const globals = [];
        const global = utility.getValueByKeyPath(newTestcase.data, 'variables.global');
        if (global) {
          Object.keys(global).forEach((gName) => {
            if (global[gName]['@value']) {
              globals.push(gName);
            }
          });
        }
        const checked = '☑';
        const unChecked = '□';
        const typeNameCheckbox = {
          GN: unChecked,
          BJ: unChecked,
          LJ: unChecked,
          XN: unChecked,
          JK: unChecked,
        };
        if (newTestcase.typeName) {
          typeNameCheckbox[newTestcase.typeName] = checked;
        }

        collectFunc.tests.push({
          testIndex,
          description: newTestcase.description || '',
          prelude: transformer.prelude || [],
          testBranches: branchExprs,
          input: newInputDetails,
          output: newOutputDetails,
          stubModules: stubModules.join(','),
          globals: globals.join(' ') || '无',
          typeName: newTestcase.typeName,
          excuted,
          passed,
          failed,
          typeNameCheckbox,
        });
        testIndex += 1;
      });
      // eslint-disable-next-line consistent-return
      return collectFunc;
    });
    collectFuncs.filter((item) => !!item).forEach((func) => {
      if (func.tests.length > 0) {
        collectFile.functions.push(func);
      }
    });
    if (collectFile.functions.length > 0) {
      this.testRecords.files.push(collectFile);
    }
  }

  // 重写对象的getAreas方法，设置要组装的报告数据
  overridingGetAreas() {
    this.ReportTestCaseTransformer.prototype.getAreas = function getAreas() {
      if (this.stubsData && this.stubsData.length > 0) {
        this.stubsData.forEach((stub) => {
          if (stub.name) {
            // stub.name += '被打桩';
          }
        });
      }
      return [this.stubsData];
    };
  }

  /**
   * 变量展开平铺
   * @param {Object} variable 变量数据
   * @param {Array[String]} resVariables 平铺好的变量
   * @param {Number} scope 输入输出域
   * @param {Number} resultKind 测试用例执行状态
   */
  variableRecursion(variable, resVariables = [], scope) {
    if (variable.children) {
      variable.children.forEach((childVariable) => {
        if (variable.baseVariableName) {
          childVariable.baseVariableName = variable.baseVariableName;
        }
        this.variableRecursion(childVariable, resVariables, scope);
      });
    } else {
      let newVariableName = variable.fieldPath.replace(/output\.|variables\.|params\.|malloc\.|global\.|statics\.|\.@value|\.@init|\.@ctor/g, '');
      // 局部静态变量需要特殊处理
      if (variable.kind === VARIABLE_KIND.statics) {
        newVariableName = newVariableName.replace(/^[\d]+|^\d+$/, `${variable.baseVariableName}`);
      }
      // 处理数组元素
      const arryIndexReg = /\.[\d]+/g;
      if (newVariableName.match(arryIndexReg)) {
        const arrayIndexs = newVariableName.match(arryIndexReg);
        arrayIndexs.forEach((arryIndexStr) => {
          newVariableName = newVariableName.replace(arryIndexStr, `[${arryIndexStr.split('.')[1]}]`);
        });
      }
      // 处理简单返回值
      if (newVariableName === '%') {
        newVariableName = 'return';
      }
      // 处理复杂结构返回值
      if (newVariableName.match(/%\./)) {
        newVariableName = newVariableName.replace(/%\./, 'return ');
      }
      newVariableName = newVariableName.replace(this.unusualCharReg, '');
      let value = variable.data && variable.data['@value'] ? variable.data['@value'] : '';
      const result = variable.data && variable.data['@result'] ? variable.data['@result'] : '';
      // 报告不显示没有填值的变量
      if (!this.complete && !value) {
        return;
      }
      if (scope === VARIABLE_SCOPE_KIND.output && !value && result) {
        value = result;
      }
      if (value || result) {
        resVariables.push({
          variableName: newVariableName,
          typeName: variable.typeName,
          value,
          result,
        });
      }
    }
  }

  async generateCfg(functionId) {
    const browser = await puppeteer.launch({
      // headless: false,
      args: ['--no-sandbox', '--disable-setuid-sandbox', '--download.default-directory=/root/phoenix/download'],
    });
    const page = await browser.newPage();

    const cfgUrl = `http:/localhost/control_flow_graph/${this.version._id}/${functionId}/${this.accessToken}`;
    // console.log('cfgUrl', cfgUrl);
    await page.goto(cfgUrl);

    const downloadPath = path.join(this.outputDir, `${randomString.generate(16)}`);
    await fs.mkdir(downloadPath);
    const client = await page.target().createCDPSession();
    await client.send('Page.setDownloadBehavior', {
      behavior: 'allow',
      downloadPath,
    });

    const downloadBtn = await page.waitForXPath("//button[@class='el-button el-button--small download-cfg-button']");
    await page.waitForFunction(() => {
      const svg = document.querySelector('#container');
      return svg.getBoundingClientRect().width > 0;
    });
    await downloadBtn.click();
    await new Promise((res) => {
      const watcher = fs.watch(path.join(downloadPath, ''), async (event, fileName) => {
        // console.log(event, fileName);
        if (fileName === 'cfg.png') {
          watcher.close();
          await page.close();
          await browser.close();
          res();
        }
      });
    });
    const imagePath = path.join(downloadPath, 'cfg.png');
    const imageInfo = await probeImage(fs.createReadStream(imagePath));
    const imageHeightMax = 500;
    const imageWithMax = 600;
    return {
      output: imagePath,
      size: {
        width: imageInfo.width && imageInfo.width < imageWithMax ? imageInfo.width : imageWithMax,
        height: imageInfo.height && imageInfo.height < imageHeightMax ? imageInfo.height : imageHeightMax,
      },
    };
  }
}

module.exports = Report601;
